Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

26장. Kotlin 코드를 Java에 공개하기

지금까지 우리는 코틀린 코드를 자유롭게 써 왔습니다.

그런데 현실의 회사 프로젝트에는
자바 코드와 코틀린 코드가 함께 섞여 있는 경우가 많습니다.

이럴 때 자바 쪽에서
우리가 만든 코틀린 코드를 불러 쓰게 될 수 있습니다.

문제는, 코틀린에서는 자연스러웠던 문법이
자바에서 보면 어색하게 보일 때가 있다는 점입니다.

이 장에서는 코틀린 코드가
자바에서 어떤 모습으로 보이는지 살펴봅니다.

그리고 몇 가지 어노테이션(annotation)을 붙여서
자바 쪽에서도 편하게 쓰도록 다듬는 방법을 배웁니다.

25장이 “자바 코드를 코틀린에서 쓰는” 방향이었다면,
이 장은 그 반대인 “코틀린 코드를 자바에 공개하는” 방향입니다.


26.1 Kotlin 코드가 JVM에서 보이는 모습

코틀린도 결국 바이트코드다

1장에서 배운 내용을 잠깐 떠올려 봅시다.

코틀린 코드는 컴파일을 거쳐
바이트코드(bytecode)로 번역된 뒤 JVM에서 실행됩니다.

자바 코드도 마찬가지로
같은 바이트코드로 번역됩니다.

그래서 자바 입장에서 코틀린 코드는
결국 “또 하나의 자바 클래스“처럼 보입니다.

자바는 코틀린 문법을 모릅니다.
자바가 보는 것은 번역이 끝난 바이트코드뿐입니다.

바로 이 점이 이 장의 출발점입니다.

우리가 편하게 쓴 코틀린 문법이
번역되면 어떤 자바 모습으로 바뀌는지 알아야
자바 쪽에서 잘 쓰도록 다듬을 수 있습니다.

어노테이션이란

이 장에서는 @ 기호로 시작하는
어노테이션(annotation)을 많이 씁니다.

어노테이션은 코드에 붙이는 “쪽지“라고 생각하면 쉽습니다.

컴파일러에게
“이 코드를 이렇게 번역해 줘“라고
부탁하는 쪽지입니다.

예를 들어 @JvmStatic이라는 쪽지를 붙이면
“자바에서 static처럼 보이게 번역해 줘“라는 뜻이 됩니다.

이런 쪽지들은 코틀린 코드의 동작을 바꾸지 않습니다.
오직 “자바에서 어떻게 보일지“만 바꿉니다.

이 장에서 다루는 쪽지들

앞으로 나올 어노테이션을 미리 표로 정리해 둡니다.
지금 외울 필요는 없습니다.

어노테이션하는 일
@JvmStatic자바에서 static 멤버처럼 보이게 함
@JvmField자바에서 필드에 바로 접근하게 함
@JvmOverloads기본값 조합마다 자바 메서드를 만들어 줌
@JvmName자바에서 보이는 이름을 바꿔 줌

“이런 게 있구나” 정도로만 읽고
아래에서 하나씩 자세히 보겠습니다.


26.2 @JvmStatic

companion object 복습

먼저 11장에서 배운
컴패니언 오브젝트(companion object)를 떠올려 봅시다.

코틀린에는 자바의 static이 없습니다.
대신 클래스 안에 companion object를 두어
“클래스 이름으로 바로 부르는 멤버“를 만듭니다.

class Calculator {
    companion object {
        fun add(a: Int, b: Int): Int {
            return a + b
        }
    }
}

코틀린에서는 이렇게 부릅니다.

val result = Calculator.add(1, 2)

클래스 이름 뒤에 바로 함수를 부르니
마치 자바의 static 메서드처럼 보입니다.

자바에서는 어떻게 보일까

문제는 자바에서 이 코드를 부를 때 생깁니다.

companion object는 사실
Companion이라는 이름의 숨은 객체입니다.

그래서 자바에서는 이렇게 길게 써야 합니다.

// 자바
int result = Calculator.Companion.add(1, 2);

Companion이라는 낯선 단어가
중간에 끼어드는 것이 보입니다.

자바 개발자 입장에서는
“이게 왜 이렇게 되지?” 하고 당황하게 됩니다.

@JvmStatic으로 해결하기

이때 함수 위에 @JvmStatic을 붙입니다.

class Calculator {
    companion object {
        @JvmStatic
        fun add(a: Int, b: Int): Int {
            return a + b
        }
    }
}

이제 자바에서는
진짜 static 메서드처럼 깔끔하게 부를 수 있습니다.

// 자바
int result = Calculator.add(1, 2);

Companion이 사라진 것이 보이시나요?

정리하면 이렇습니다.

상황자바에서 부르는 모습
@JvmStatic 없음Calculator.Companion.add(1, 2)
@JvmStatic 붙임Calculator.add(1, 2)

코틀린 쪽에서 부르는 방법은
둘 다 Calculator.add(1, 2)로 똑같습니다.

즉, @JvmStatic
코틀린 사용자에게는 아무 영향이 없고
자바 사용자에게만 편리함을 줍니다.


26.3 @JvmField

코틀린의 프로퍼티 복습

7장에서 배운 프로퍼티(property)를 떠올려 봅시다.

코틀린에서 클래스에 값을 하나 두면,
겉으로는 단순한 변수처럼 보입니다.

class User {
    var name: String = "홍길동"
}

하지만 실제로는
보이지 않는 두 함수가 함께 만들어집니다.

  • 값을 읽는 함수: getter
  • 값을 바꾸는 함수: setter

게터(getter)는 값을 꺼내 오는 함수,
세터(setter)는 값을 넣어 주는 함수입니다.

코틀린에서는 이 둘을 자동으로 만들어 주기 때문에
우리는 그냥 user.name처럼 편하게 씁니다.

자바에서는 어떻게 보일까

자바에서 이 프로퍼티를 쓰면
숨어 있던 게터와 세터가 그대로 드러납니다.

// 자바
User user = new User();
user.setName("이순신");        // 값 넣기
String name = user.getName();  // 값 읽기

user.name처럼 바로 못 쓰고
getName(), setName()을 거쳐야 합니다.

대부분의 경우 이 방식이 오히려 안전합니다.
그래서 보통은 그대로 두는 편이 좋습니다.

@JvmField로 필드 직접 열기

하지만 가끔은
게터/세터 없이 값에 바로 접근하고 싶을 때가 있습니다.

이때 프로퍼티 위에 @JvmField를 붙입니다.

class User {
    @JvmField
    var name: String = "홍길동"
}

그러면 게터/세터가 만들어지지 않고,
자바에서 필드에 바로 접근하게 됩니다.

// 자바
User user = new User();
user.name = "이순신";          // 필드에 바로 넣기
String name = user.name;       // 필드에서 바로 읽기

두 방식을 비교하면 이렇습니다.

상황자바에서 접근하는 모습
@JvmField 없음user.getName() / user.setName(...)
@JvmField 붙임user.name

@JvmField는 게터/세터를 없애고
필드를 그대로 노출합니다.

한 가지 주의할 점이 있습니다.
@JvmField를 붙이면 게터/세터가 사라지므로,
나중에 값을 검사하는 로직을 게터에 넣기 어려워집니다.

그래서 꼭 필요한 상수 같은 경우가 아니면
기본 방식(게터/세터)을 그대로 두는 편이 안전합니다.


26.4 @JvmOverloads

코틀린의 기본값 매개변수 복습

4장에서 배운 기본값(default value)을 떠올려 봅시다.

코틀린에서는 매개변수에
기본값을 미리 정해 둘 수 있습니다.

fun greet(name: String = "손님", greeting: String = "안녕하세요") {
    println("$greeting, $name 님")
}

덕분에 인자를 생략하고 부를 수 있습니다.

greet()                         // 안녕하세요, 손님 님
greet("홍길동")                  // 안녕하세요, 홍길동 님
greet("홍길동", "반갑습니다")     // 반갑습니다, 홍길동 님

인자를 몇 개 넣든
코틀린이 알아서 빈자리를 기본값으로 채워 줍니다.

자바에는 기본값이 없다

그런데 자바에는
이런 “매개변수 기본값” 기능이 아예 없습니다.

그래서 자바에서 위 함수를 부르면,
기본값을 쓰지 못하고 항상 두 인자를 모두 넣어야 합니다.

// 자바
greet("홍길동", "반갑습니다");   // 두 개 다 넣어야 함
greet("홍길동");                // 오류: 이런 메서드 없음
greet();                       // 오류: 이런 메서드 없음

코틀린에서 편했던 “생략하고 부르기“가
자바에서는 통하지 않는 것입니다.

@JvmOverloads로 여러 버전 만들기

이때 함수 위에 @JvmOverloads를 붙입니다.

@JvmOverloads
fun greet(name: String = "손님", greeting: String = "안녕하세요") {
    println("$greeting, $name 님")
}

그러면 컴파일러가
인자 개수별로 여러 버전의 메서드를 자동으로 만들어 줍니다.

여기서 오버로드(overload)란
“이름은 같지만 매개변수가 다른 메서드를 여러 개 두는 것“을 뜻합니다.

만들어지는 자바 메서드는 이렇게 세 개입니다.

// 자바에서 보이는 모습 (자동 생성)
void greet();
void greet(String name);
void greet(String name, String greeting);

이제 자바에서도
인자를 생략하며 부를 수 있습니다.

// 자바
greet();                        // 안녕하세요, 손님 님
greet("홍길동");                 // 안녕하세요, 홍길동 님
greet("홍길동", "반갑습니다");    // 반갑습니다, 홍길동 님

정리하면 이렇습니다.

@JvmOverloads
기본값 조합마다 자바 메서드를 만들어 주어
자바에서도 인자를 생략할 수 있게 해 줍니다.


26.5 @JvmName

이름이 부딪히는 경우

가끔 코틀린에서는 문제가 없는데
자바에서는 이름이 충돌하는 경우가 있습니다.

대표적인 예가 시그니처 충돌입니다.

시그니처(signature)란
“함수의 이름 + 매개변수 목록“을 합친,
함수를 구분하는 이름표라고 생각하면 됩니다.

아래 두 함수를 봅시다.

fun List<Int>.sum(): Int {
    // 정수 합계
    return this.fold(0) { acc, n -> acc + n }
}

fun List<String>.sum(): String {
    // 문자열 이어 붙이기
    return this.joinToString("")
}

코틀린에서는 이 둘을 구분할 수 있습니다.
List<Int>인지 List<String>인지 알기 때문입니다.

하지만 자바로 번역되면
제네릭 정보가 흐려져서 두 함수의 이름표가 똑같아집니다.
그래서 “이름이 겹친다“는 오류가 납니다.

(제네릭은 20장에서 자세히 다룹니다.)

@JvmName으로 이름 바꾸기

이때 @JvmName으로
자바에서 보이는 이름만 다르게 지정합니다.

@JvmName("sumOfInt")
fun List<Int>.sum(): Int {
    return this.fold(0) { acc, n -> acc + n }
}

@JvmName("sumOfString")
fun List<String>.sum(): String {
    return this.joinToString("")
}

이제 자바에서는
서로 다른 이름으로 부를 수 있습니다.

// 자바
int total = MyUtilsKt.sumOfInt(numbers);
String joined = MyUtilsKt.sumOfString(words);

한편 코틀린에서는
여전히 원래 이름 sum으로 부릅니다.

val total = numbers.sum()
val joined = words.sum()

정리하면 이렇습니다.

@JvmName
코틀린 이름은 그대로 두고,
자바에서 보이는 이름만 바꿔 줍니다.

@JvmName은 다음 절에서 볼
파일 단위 이름을 바꿀 때도 쓰입니다.


26.6 Top-level Function (파일명Kt 클래스)

톱레벨 함수 복습

1장과 4장에서 배운
톱레벨 함수(top-level function)를 떠올려 봅시다.

코틀린에서는 클래스 밖,
즉 파일에 함수를 바로 놓을 수 있었습니다.

// 파일 이름: MathUtils.kt

fun square(x: Int): Int {
    return x * x
}

코틀린에서는 아주 자연스럽게 부릅니다.

val result = square(5)

클래스 없이 함수만 덜렁 있으니
코드가 무척 간결합니다.

자바에는 클래스 밖 함수가 없다

그런데 자바에는
“클래스 밖의 함수“라는 개념이 없습니다.

자바의 모든 메서드는
반드시 어떤 클래스 안에 들어 있어야 합니다.

그래서 코틀린 컴파일러는
톱레벨 함수를 담을 클래스를 하나 몰래 만들어 줍니다.

규칙은 간단합니다.
“파일 이름 + Kt“가 클래스 이름이 됩니다.

MathUtils.kt 파일에 있던 함수는
MathUtilsKt라는 클래스의 static 메서드가 됩니다.

그래서 자바에서는 이렇게 부릅니다.

// 자바
int result = MathUtilsKt.square(5);

파일 이름에서 온 MathUtilsKt
클래스 이름으로 등장하는 것이 보입니다.

@JvmName으로 클래스 이름 바꾸기

~Kt라는 이름이 어색하게 느껴질 수 있습니다.

이때 파일 맨 위에서 @file:JvmName을 쓰면
그 클래스 이름을 우리가 원하는 대로 바꿀 수 있습니다.

@file:JvmName("MathUtils")

package com.example.util

fun square(x: Int): Int {
    return x * x
}

맨 앞의 @file:
“이 어노테이션은 파일 전체에 적용한다“는 표시입니다.

이제 자바에서는
깔끔한 이름으로 부를 수 있습니다.

// 자바
int result = MathUtils.square(5);

정리하면 이렇습니다.

상황자바에서 부르는 모습
기본 (파일명 MathUtils.kt)MathUtilsKt.square(5)
@file:JvmName("MathUtils")MathUtils.square(5)

26.7 Java 친화적인 Kotlin API 만들기

API란 무엇인가

지금까지 배운 것을 하나로 묶어 봅시다.

여기서 API(Application Programming Interface)란
“남이 가져다 쓰라고 열어 둔 코드의 겉모습“입니다.

내가 만든 코틀린 함수와 클래스를
자바 개발자가 불러 쓴다면,
그것은 그 사람에게 하나의 API가 됩니다.

좋은 API는
쓰는 사람이 헷갈리지 않게 만든 API입니다.

그래서 자바에서도 쓸 코틀린 코드라면,
자바 쪽에서 어떻게 보일지 미리 챙겨 두어야 합니다.

체크리스트

이 장에서 배운 어노테이션을
“자바에서 어색할 때 붙이는 처방“으로 정리해 봅시다.

자바에서 어색한 점붙일 처방
Companion이 끼어들어 지저분함@JvmStatic
getX() / setX()가 번거로움@JvmField
기본값을 못 써서 인자를 다 넣어야 함@JvmOverloads
이름이 겹치거나 어색함@JvmName

이 표 하나만 기억해 두어도
대부분의 상황에 대응할 수 있습니다.

하나로 모아 보기

여러 처방을 함께 쓴 예를 봅시다.

@file:JvmName("UserService")

package com.example.user

class User {
    companion object {
        @JvmStatic
        fun createGuest(): User {
            return User()
        }
    }

    @JvmOverloads
    fun greet(greeting: String = "안녕하세요") {
        println(greeting)
    }
}

이 코드를 자바에서 쓰면 이렇게 됩니다.

// 자바
User guest = User.createGuest();  // @JvmStatic 덕분에 깔끔
guest.greet();                    // @JvmOverloads 덕분에 생략 가능
guest.greet("반갑습니다");         // 인자를 넣어도 됨

어노테이션이 없었다면
User.Companion.createGuest()처럼 지저분했을 것입니다.

언제 신경 쓰고, 언제 넘어갈까

마지막으로 균형을 잡아 봅시다.

  • 코틀린만 쓰는 프로젝트라면
    이 어노테이션들은 대부분 필요 없습니다.
  • 자바와 코틀린을 섞어 쓰거나,
    자바에서 쓸 라이브러리를 만든다면
    이 어노테이션들이 큰 도움이 됩니다.

즉, 무조건 다 붙일 필요는 없습니다.

“이 코드를 자바에서도 부를까?”
이 질문에 “그렇다“일 때만 처방을 꺼내면 됩니다.

이렇게 자바 쪽을 배려하는 습관이
두 언어가 섞인 실무 프로젝트에서
당신을 신뢰받는 개발자로 만들어 줍니다.


26장을 마치며

이 장에서 우리는 다음을 배웠습니다.

  • 코틀린 코드도 결국 바이트코드로 번역되어,
    자바에서는 “또 하나의 자바 클래스“로 보인다는 점
  • @JvmStatic으로 companion object 멤버를
    자바의 static처럼 깔끔하게 부르게 하는 방법
  • @JvmField로 게터/세터 없이
    필드에 바로 접근하게 하는 방법
  • @JvmOverloads로 기본값 조합마다
    자바 메서드를 만들어 주는 방법
  • @JvmName으로 자바에서 보이는
    함수 이름과 클래스 이름을 바꾸는 방법
  • 톱레벨 함수가 파일명Kt 클래스로
    번역된다는 점과 그 이름을 바꾸는 방법

핵심은 하나입니다.
“이 코드를 자바에서도 부를까?“를 먼저 묻는 것입니다.

그 답이 “그렇다“일 때,
이 장의 처방들을 하나씩 꺼내 쓰면 됩니다.

다음 장에서는
코틀린을 더 깊이 있게 다루는 주제로 넘어갑니다.